iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
Modern Web

從 Web 到宇宙:Three.js × WebGL 互動式太陽系與光影實作系列 第 27 篇

Day 27 - Planet Focus × Planet Info:鎖定行星與資訊面板

  • 分享至 

  • xImage
  •  

今天要把「選中行星」這件事真正利用起來。
也就是點擊 Earth 之後 Camera 開始鎖定 Earth,
OrbitControls.target 跟著 Earth 移動,
左下角顯示 Earth 資訊。
另外也會加入返回太陽系讓 Camera 回到以 Sun 為中心的模式。

由於目前專案程式碼已逐漸增加,從 Day 23 開始文章只列出當日新增或修改的程式碼。
完整整合版將於 Day 30 最終完成時提供。

一、今天完成目標

今天會完成:

  • 新增 Planet Focus Mode
  • 點擊行星後讓 Camera 鎖定該行星
  • 讓 OrbitControls.target 每幀跟著行星移動
  • 保存目前選中的 Planet
  • 新增 Planet Info Panel
  • 顯示行星中文名稱與基本資料
  • 新增「返回太陽系」按鈕
  • 回到以 Sun 為中心的 Camera 模式

二、本日修改摘要

項目 修改內容
Raycaster 點擊 Planet 後儲存 selectedPlanet
Camera 進入 Focus Mode
OrbitControls target 每一幀更新到 Planet Position
UI 新增 Planet Info Panel
Planet Data 加入中文名稱、簡介等資訊
Return Button 返回 Sun Center Mode

三、先新增 Planet 資訊

原本 planetData 只有:

{
    name: "Earth",
    size: 0.7,
    distance: 8.5,
    rotationSpeed: 1.0,
    orbitSpeed: 0.8,
    texture: "textures/earth.jpg"
}

今天可以多放一些資訊:

{
    name: "Earth",
    chineseName: "地球",
    description: "目前已知唯一存在生命的行星",
    order: "第三顆行星",
    distance: 8.5,
    size: 0.7,
    rotationSpeed: 1.0,
    orbitSpeed: 0.8,
    texture: "textures/earth.jpg"
}

這些資料不一定要完全按照真實天文單位。
今天主要目的是 選到哪顆 Planet 就顯示哪顆 Planet 的資料
其他行星也照這個格式補上。

四、Planet Object 也保存資訊

原本:

planets.push({

    name:
        data.name,

    mesh:
        planet,

    distance:
        data.distance,

    rotationSpeed:
        data.rotationSpeed,

    orbitSpeed:
        data.orbitSpeed,

    angle:
        startAngle

});

今天改成:

planets.push({

    name:
        data.name,

    chineseName:
        data.chineseName,

    description:
        data.description,

    order:
        data.order,

    mesh:
        planet,

    size:
        data.size,

    distance:
        data.distance,

    rotationSpeed:
        data.rotationSpeed,

    orbitSpeed:
        data.orbitSpeed,

    angle:
        startAngle

});

這樣 Raycaster 找到 Planet 後,就可以直接取得:

selectedPlanet.chineseName
selectedPlanet.description
selectedPlanet.order

五、新增 selectedPlanet

Day 26 裡我們只是在 Click Event 裡暫時建立:

const selectedPlanet =
    planets.find(...);

今天要讓這個 Planet 持續被記住。
所以改成:

let selectedPlanet =
    null;

放在外層。
代表:
null → 沒有鎖定 Planet
Earth Object → 正在鎖定 Earth
Saturn Object → 正在鎖定 Saturn

六、新增 Focus Mode

再準備:

let focusMode =
    false;

可以理解成:
false → Camera 以 Sun 為中心
true → Camera 跟隨 selectedPlanet

七、點擊 Planet 時進入 Focus Mode

Day 26 原本:

if (
    selectedPlanet
) {

    highlightOrbit(
        selectedPlanet.name
    );

}

今天改成:

if (
    clickedPlanet
) {

    selectedPlanet =
        clickedPlanet;

    focusMode =
        true;


    highlightOrbit(
        selectedPlanet.name
    );


    showPlanetInfo(
        selectedPlanet
    );

}

這裡我把 selectedPlanet 留給外層狀態使用。
所以 Click Event 裡暫時找到的物件改名,
clickedPlanet 比較不容易搞混。

八、Camera 要怎麼靠近 Planet?

點擊後不能直接:

camera.position.copy(
    selectedPlanet.mesh.position
);

因為這樣 Camera 會跑到 Planet 裡面,
所以要保留一點距離。
例如:

const focusOffset =
    new THREE.Vector3(
        4,
        2,
        4
    );

然後:

camera.position.copy(
    selectedPlanet.mesh.position
);

camera.position.add(
    focusOffset
);

這樣 Camera 就會在 Planet 附近,而不是重疊在同一個位置。

九、根據 Planet 大小調整 Camera 距離

但 Jupiter 和 Mercury 大小不同。
如果全部使用 (4, 2, 4) Jupiter 可能太近,Mercury 又可能太遠。
所以可以根據 selectedPlanet.size 計算:

const focusDistance =
    selectedPlanet.size *
    6 +
    3;

接著:

camera.position.set(

    selectedPlanet.mesh.position.x +
        focusDistance,

    selectedPlanet.mesh.position.y +
        focusDistance *
        0.5,

    selectedPlanet.mesh.position.z +
        focusDistance

);

這樣:
大 Planet → Camera 稍微遠一點
小 Planet → Camera 稍微近一點
畫面會比較自然。

十、Focus Planet 最大的問題

Planet 還在公轉
所以如果只在點擊時:

controls.target.copy(
    selectedPlanet.mesh.position
);

下一秒 Planet 就已經跑走了。
會變成:
Camera Target → 留在舊位置
Planet → 繼續公轉離開

所以不能只設定一次。

十一、每一幀更新 controls.target

在 animate() 裡加入:

if (
    focusMode &&
    selectedPlanet
) {

    controls.target.copy(
        selectedPlanet.mesh.position
    );

}

這樣每一幀:
Earth 移動 → controls.target 更新 → Camera 持續看著 Earth
這就是 Planet Focus Mode 最重要的地方。

十二、Camera 也要跟著 Planet 移動嗎?

如果只有 controls.target.copy(...)
Camera 會一直看著 Planet。
但是 Planet 公轉越久,Camera 跟 Planet 的距離會逐漸改變。
所以更好的做法是保存:

const focusCameraOffset =
    new THREE.Vector3();

當選擇 Planet 時:

focusCameraOffset
    .copy(
        camera.position
    )
    .sub(
        selectedPlanet.mesh.position
    );

意思就是 Camera Position - Planet Position = Camera 相對 Planet 的位置
之後每一幀:

camera.position.copy(
    selectedPlanet.mesh.position
);

camera.position.add(
    focusCameraOffset
);

這樣 Planet 移動多少,Camera 也跟著移動多少。

十三、建立 Planet Info Panel

接著修改 index.html。
在 Control Panel 之外加入:

<div
    class="planet-info"
    id="planetInfo"
>

    <h2 id="planetName">
        Planet
    </h2>

    <p id="planetChineseName"></p>

    <p id="planetOrder"></p>

    <p id="planetDescription"></p>

    <button id="returnButton">
        返回太陽系
    </button>

</div>

一開始不需要顯示。

十四、Planet Info CSS

加入:

.planet-info {
    position: fixed;

    left: 40px;
    bottom: 40px;

    z-index: 20;

    width: 280px;

    padding: 20px;

    background:
        rgba(
            15,
            15,
            25,
            0.82
        );

    border:
        1px solid
        rgba(
            255,
            255,
            255,
            0.15
        );

    border-radius: 12px;

    backdrop-filter:
        blur(10px);

    display: none;
}


.planet-info.active {
    display: block;
}


.planet-info h2 {
    margin-top: 0;
}


.planet-info p {
    margin: 8px 0;

    color: #cccccc;

    font-size: 14px;

    line-height: 1.5;
}


.planet-info button {
    width: 100%;

    margin-top: 14px;

    padding: 10px;

    border: none;

    border-radius: 8px;

    background:
        rgba(
            255,
            255,
            255,
            0.12
        );

    color: white;

    cursor: pointer;
}

十五、取得 Planet Info DOM

JavaScript

const planetInfo =
    document.getElementById(
        "planetInfo"
    );

const planetName =
    document.getElementById(
        "planetName"
    );

const planetChineseName =
    document.getElementById(
        "planetChineseName"
    );

const planetOrder =
    document.getElementById(
        "planetOrder"
    );

const planetDescription =
    document.getElementById(
        "planetDescription"
    );

const returnButton =
    document.getElementById(
        "returnButton"
    );

十六、顯示 Planet Info

建立:

function showPlanetInfo(
    planet
) {

    planetName.textContent =
        planet.name;


    planetChineseName.textContent =
        "中文名稱:" +
        planet.chineseName;


    planetOrder.textContent =
        "位置:" +
        planet.order;


    planetDescription.textContent =
        planet.description;


    planetInfo.classList.add(
        "active"
    );

}

例如點 Earth:

Earth
中文名稱:地球
位置:第三顆行星

目前已知唯一存在生命的行星

十七、Focus Planet Function

今天可以把 Focus 邏輯整理成一個 Function:

function focusPlanet(
    planet
) {

    selectedPlanet =
        planet;

    focusMode =
        true;


    const focusDistance =
        planet.size *
        6 +
        3;


    camera.position.set(

        planet.mesh.position.x +
            focusDistance,

        planet.mesh.position.y +
            focusDistance *
            0.5,

        planet.mesh.position.z +
            focusDistance

    );


    focusCameraOffset
        .copy(
            camera.position
        )
        .sub(
            planet.mesh.position
        );


    controls.target.copy(
        planet.mesh.position
    );


    controls.update();


    showPlanetInfo(
        planet
    );

}

所以 Raycaster 選中後只需要:

focusPlanet(
    clickedPlanet
);

十八、返回太陽系

接下來建立:

function returnToSolarSystem() {

    focusMode =
        false;

    selectedPlanet =
        null;


    camera.position.set(
        30,
        25,
        30
    );


    controls.target.set(
        0,
        0,
        0
    );


    controls.update();


    planetInfo.classList.remove(
        "active"
    );


    resetOrbitColors();

}

按鈕:

returnButton.addEventListener(
    "click",
    returnToSolarSystem
);

這樣:
Planet Focus → 返回太陽系 → Camera 回 Sun
→ Info Panel 隱藏 → Orbit 恢復灰色

十九、Keyboard Camera 也要退出 Focus Mode

這裡還有一個小細節。
如果目前正在 Earth Focus
然後使用者按數字鍵,
我們應該讓 Focus Mode 解除。
所以 Keyboard Event 一開始加入:

focusMode =
    false;

selectedPlanet =
    null;

planetInfo.classList.remove(
    "active"
);

這樣 Camera Shortcut 才不會下一幀又被 Earth 拉回去。

二十、今天動畫的 Focus 邏輯

animate() 最後,在 controls.update() 之前加入:

if (
    focusMode &&
    selectedPlanet
) {

    camera.position.copy(
        selectedPlanet.mesh.position
    );

    camera.position.add(
        focusCameraOffset
    );


    controls.target.copy(
        selectedPlanet.mesh.position
    );

}

因此:
Planet 公轉 → Planet Position 改變→ Camera Position 跟著移動
→ controls.target 跟著移動 → 畫面持續鎖定 Planet

二十一、今天小結

今天最重要的不是單純 controls.target.copy(...)
而是要理解 Planet 正在移動,所以 Focus Mode 必須每一幀更新 Camera,
才能真的跟著 Planet。

現在使用者已經可以:
點 Earth
→ Camera 鎖定 Earth
→ Earth Orbit Highlight
→ 顯示 Earth Info

點 Saturn
→ Camera 鎖定 Saturn
→ Saturn Orbit Highlight
→ 顯示 Saturn Info

返回太陽系
→ 回到 Sun Center Mode

Day 28 預告

「Responsive × Performance:讓太陽系在不同裝置更順暢」

目前功能已經很多:

8 Planets
Moon
Venus Atmosphere
Saturn Ring
Starfield
Shadow
OrbitControls
UI
Animation Controls
Raycaster
Planet Focus

Day 28 要開始整理效能。
我們會處理 不同螢幕尺寸和 UI Responsive

高 DPI 螢幕 → Pixel Ratio 限制
大量 Geometry → 降低不必要 Segment
Shadow → 只保留必要物件
手機 / 小螢幕 → 調整 UI

為最後 Day 29 的整合 Debug,以及 Day 30 最終作品做準備。


上一篇
Day 26 - Raycaster:用滑鼠選擇行星
下一篇
Day 28 - Responsive × Performance:讓太陽系在不同裝置更順暢
系列文
從 Web 到宇宙:Three.js × WebGL 互動式太陽系與光影實作 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言